fix: guard workspace auth refresh races#11726
Conversation
📝 WalkthroughWalkthroughAdds token expiry tracking, schedules refresh retries based on remaining lifetime, prevents commits from stale in-flight refreshes, and changes retry exhaustion behavior to preserve valid workspace tokens on transient errors. Tests updated to assert preserved state and improve sessionStorage resilience. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant WorkspaceAuthStore
participant SessionStorage
participant Timer
Client->>WorkspaceAuthStore: trigger refreshToken()
WorkspaceAuthStore->>Timer: schedule retry/backoff (scheduleTokenRefreshRetry)
Timer-->>WorkspaceAuthStore: retry delay elapsed
WorkspaceAuthStore->>WorkspaceAuthStore: check refreshRequestId (stale?) and hasValidWorkspaceToken()
alt request is current
WorkspaceAuthStore->>SessionStorage: set CURRENT_WORKSPACE / TOKEN / EXPIRES
WorkspaceAuthStore-->>Client: commit success
else stale or transient TOKEN_EXCHANGE_FAILED
WorkspaceAuthStore-->>Timer: schedule another retry (if token still valid)
WorkspaceAuthStore-->>Client: preserve current state / do not clear
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~22 minutes Poem
🚥 Pre-merge checks | ✅ 6 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (6 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning Review ran into problems🔥 ProblemsGit: Failed to clone repository. Please run the Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…auth-refresh-race-v2
🎨 Storybook: ✅ Built — View Storybook |
🎭 Playwright: ✅ 1399 passed, 0 failed📊 Browser Reports
|
📦 Bundle: 5.23 MB gzip 🔴 +362 BDetailsSummary
Category Glance App Entry Points — 22.5 kB (baseline 22.5 kB) • ⚪ 0 BMain entry bundles and manifests
Status: 1 added / 1 removed Graph Workspace — 1.24 MB (baseline 1.24 MB) • ⚪ 0 BGraph editor runtime, canvas, workflow orchestration
Status: 1 added / 1 removed Views & Navigation — 77.7 kB (baseline 77.7 kB) • ⚪ 0 BTop-level views, pages, and routed surfaces
Status: 9 added / 9 removed / 2 unchanged Panels & Settings — 484 kB (baseline 484 kB) • ⚪ 0 BConfiguration panels, inspectors, and settings screens
Status: 10 added / 10 removed / 11 unchanged User & Accounts — 17.4 kB (baseline 17.4 kB) • ⚪ 0 BAuthentication, profile, and account management bundles
Status: 5 added / 5 removed / 2 unchanged Editors & Dialogs — 113 kB (baseline 113 kB) • ⚪ 0 BModals, dialogs, drawers, and in-app editors
Status: 4 added / 4 removed UI Components — 61 kB (baseline 61 kB) • ⚪ 0 BReusable component library chunks
Status: 5 added / 5 removed / 8 unchanged Data & Services — 3.04 MB (baseline 3.04 MB) • 🔴 +1.38 kBStores, services, APIs, and repositories
Status: 13 added / 13 removed / 4 unchanged Utilities & Hooks — 363 kB (baseline 363 kB) • ⚪ 0 BHelpers, composables, and utility bundles
Status: 13 added / 13 removed / 18 unchanged Vendor & Third-Party — 9.88 MB (baseline 9.88 MB) • ⚪ 0 BExternal libraries and shared vendor chunks Status: 16 unchanged Other — 8.83 MB (baseline 8.83 MB) • ⚪ 0 BBundles that do not match a named category
Status: 57 added / 57 removed / 78 unchanged ⚡ Performance Report
All metrics
Historical variance (last 15 runs)
Trend (last 15 commits on main)
Raw data{
"timestamp": "2026-04-28T15:30:39.214Z",
"gitSha": "e7612785d9a60148b43dc686a070bc301a44562d",
"branch": "ben/fe-485-workspace-auth-refresh-race-v2",
"measurements": [
{
"name": "canvas-idle",
"durationMs": 1993.754999999993,
"styleRecalcs": 8,
"styleRecalcDurationMs": 6.613,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 396.84200000000004,
"heapDeltaBytes": 20536688,
"heapUsedBytes": 65135232,
"domNodes": 16,
"jsHeapTotalBytes": 22282240,
"scriptDurationMs": 20.741000000000003,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "canvas-idle",
"durationMs": 2016.1959999999794,
"styleRecalcs": 11,
"styleRecalcDurationMs": 9.905000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 416.485,
"heapDeltaBytes": -4072232,
"heapUsedBytes": 46434000,
"domNodes": 22,
"jsHeapTotalBytes": 24641536,
"scriptDurationMs": 24.372000000000003,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "canvas-idle",
"durationMs": 2030.0690000000259,
"styleRecalcs": 10,
"styleRecalcDurationMs": 10.634,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 426.50200000000007,
"heapDeltaBytes": -4000232,
"heapUsedBytes": 46084320,
"domNodes": 20,
"jsHeapTotalBytes": 24117248,
"scriptDurationMs": 26.83,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-mouse-sweep",
"durationMs": 2005.1670000000001,
"styleRecalcs": 79,
"styleRecalcDurationMs": 43.126,
"layouts": 12,
"layoutDurationMs": 3.058,
"taskDurationMs": 987.9239999999999,
"heapDeltaBytes": 15747152,
"heapUsedBytes": 59863108,
"domNodes": 63,
"jsHeapTotalBytes": 23330816,
"scriptDurationMs": 124.02199999999999,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-mouse-sweep",
"durationMs": 2005.7130000000143,
"styleRecalcs": 84,
"styleRecalcDurationMs": 43.706,
"layouts": 12,
"layoutDurationMs": 3.615,
"taskDurationMs": 1004.5880000000001,
"heapDeltaBytes": 16804676,
"heapUsedBytes": 60562792,
"domNodes": 66,
"jsHeapTotalBytes": 23855104,
"scriptDurationMs": 131.658,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-mouse-sweep",
"durationMs": 2033.2540000000563,
"styleRecalcs": 84,
"styleRecalcDurationMs": 46.599000000000004,
"layouts": 12,
"layoutDurationMs": 3.679,
"taskDurationMs": 992.273,
"heapDeltaBytes": 15741860,
"heapUsedBytes": 59795972,
"domNodes": 66,
"jsHeapTotalBytes": 23330816,
"scriptDurationMs": 123.77600000000001,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1722.300999999959,
"styleRecalcs": 32,
"styleRecalcDurationMs": 19.523,
"layouts": 6,
"layoutDurationMs": 0.6479999999999999,
"taskDurationMs": 340.702,
"heapDeltaBytes": 120192,
"heapUsedBytes": 50198284,
"domNodes": 80,
"jsHeapTotalBytes": 24641536,
"scriptDurationMs": 28.918000000000006,
"eventListeners": 19,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1723.9930000000072,
"styleRecalcs": 31,
"styleRecalcDurationMs": 18.531,
"layouts": 6,
"layoutDurationMs": 0.583,
"taskDurationMs": 345.34,
"heapDeltaBytes": 25059248,
"heapUsedBytes": 68404440,
"domNodes": 78,
"jsHeapTotalBytes": 21233664,
"scriptDurationMs": 29.375000000000004,
"eventListeners": 19,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1756.7050000000108,
"styleRecalcs": 32,
"styleRecalcDurationMs": 18.109,
"layouts": 6,
"layoutDurationMs": 0.5789999999999998,
"taskDurationMs": 340.98499999999996,
"heapDeltaBytes": 25035484,
"heapUsedBytes": 68707168,
"domNodes": 80,
"jsHeapTotalBytes": 21495808,
"scriptDurationMs": 29.685999999999996,
"eventListeners": 19,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "dom-widget-clipping",
"durationMs": 535.8960000000081,
"styleRecalcs": 11,
"styleRecalcDurationMs": 8.068,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 332.209,
"heapDeltaBytes": 6754688,
"heapUsedBytes": 50941180,
"domNodes": 18,
"jsHeapTotalBytes": 12582912,
"scriptDurationMs": 55.81999999999999,
"eventListeners": 2,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999727
},
{
"name": "dom-widget-clipping",
"durationMs": 540.6289999999672,
"styleRecalcs": 11,
"styleRecalcDurationMs": 7.531,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 339.7560000000001,
"heapDeltaBytes": 6988660,
"heapUsedBytes": 50677372,
"domNodes": 18,
"jsHeapTotalBytes": 12582912,
"scriptDurationMs": 55.053000000000004,
"eventListeners": 2,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.663333333333338,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "dom-widget-clipping",
"durationMs": 521.6739999999618,
"styleRecalcs": 13,
"styleRecalcDurationMs": 8.412,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 320.06600000000003,
"heapDeltaBytes": 7078280,
"heapUsedBytes": 50949676,
"domNodes": 22,
"jsHeapTotalBytes": 13107200,
"scriptDurationMs": 53.33200000000001,
"eventListeners": 2,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999727
},
{
"name": "large-graph-idle",
"durationMs": 2019.2720000000008,
"styleRecalcs": 9,
"styleRecalcDurationMs": 8.466999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 611.2950000000001,
"heapDeltaBytes": -530200,
"heapUsedBytes": 52436624,
"domNodes": -258,
"jsHeapTotalBytes": 16158720,
"scriptDurationMs": 110.22300000000001,
"eventListeners": -127,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-idle",
"durationMs": 2039.9919999999838,
"styleRecalcs": 10,
"styleRecalcDurationMs": 9.774000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 605.134,
"heapDeltaBytes": 3019668,
"heapUsedBytes": 57210936,
"domNodes": -260,
"jsHeapTotalBytes": 16158720,
"scriptDurationMs": 107.516,
"eventListeners": -125,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-idle",
"durationMs": 2024.630000000002,
"styleRecalcs": 9,
"styleRecalcDurationMs": 8.258999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 561.7570000000001,
"heapDeltaBytes": 4240700,
"heapUsedBytes": 56862008,
"domNodes": -261,
"jsHeapTotalBytes": 16158720,
"scriptDurationMs": 105.553,
"eventListeners": -125,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-pan",
"durationMs": 2103.240000000028,
"styleRecalcs": 68,
"styleRecalcDurationMs": 17.722999999999995,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1113.131,
"heapDeltaBytes": -16057728,
"heapUsedBytes": 48393432,
"domNodes": -264,
"jsHeapTotalBytes": 14962688,
"scriptDurationMs": 386.898,
"eventListeners": -125,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "large-graph-pan",
"durationMs": 2118.7229999999886,
"styleRecalcs": 68,
"styleRecalcDurationMs": 17.658000000000005,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1125.1630000000002,
"heapDeltaBytes": 9881820,
"heapUsedBytes": 73253360,
"domNodes": -261,
"jsHeapTotalBytes": 15953920,
"scriptDurationMs": 395.27299999999997,
"eventListeners": -125,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "large-graph-pan",
"durationMs": 2129.1290000000345,
"styleRecalcs": 68,
"styleRecalcDurationMs": 16.778000000000002,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1112.288,
"heapDeltaBytes": 18709572,
"heapUsedBytes": 73818132,
"domNodes": -265,
"jsHeapTotalBytes": 18460672,
"scriptDurationMs": 390.156,
"eventListeners": -125,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66999999999998,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "large-graph-zoom",
"durationMs": 3182.278999999994,
"styleRecalcs": 66,
"styleRecalcDurationMs": 19.862999999999996,
"layouts": 60,
"layoutDurationMs": 7.72,
"taskDurationMs": 1367.2300000000002,
"heapDeltaBytes": 6022572,
"heapUsedBytes": 62731940,
"domNodes": -266,
"jsHeapTotalBytes": 17731584,
"scriptDurationMs": 492.93600000000004,
"eventListeners": -123,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-zoom",
"durationMs": 3195.707999999968,
"styleRecalcs": 64,
"styleRecalcDurationMs": 18.284,
"layouts": 60,
"layoutDurationMs": 7.898000000000001,
"taskDurationMs": 1363.857,
"heapDeltaBytes": -11546724,
"heapUsedBytes": 54436136,
"domNodes": -269,
"jsHeapTotalBytes": 13914112,
"scriptDurationMs": 481.01899999999995,
"eventListeners": -125,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-zoom",
"durationMs": 3161.811999999941,
"styleRecalcs": 66,
"styleRecalcDurationMs": 21.444000000000003,
"layouts": 60,
"layoutDurationMs": 7.804,
"taskDurationMs": 1387.8999999999999,
"heapDeltaBytes": 2144604,
"heapUsedBytes": 57819524,
"domNodes": -265,
"jsHeapTotalBytes": 16945152,
"scriptDurationMs": 494.738,
"eventListeners": -123,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "minimap-idle",
"durationMs": 2007.6589999999896,
"styleRecalcs": 9,
"styleRecalcDurationMs": 9.457,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 593.537,
"heapDeltaBytes": 2532364,
"heapUsedBytes": 58770964,
"domNodes": -263,
"jsHeapTotalBytes": 15896576,
"scriptDurationMs": 100.11200000000001,
"eventListeners": -125,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.670000000000012,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "minimap-idle",
"durationMs": 2053.9920000000507,
"styleRecalcs": 9,
"styleRecalcDurationMs": 8.671999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 593.8860000000001,
"heapDeltaBytes": 17189488,
"heapUsedBytes": 72951656,
"domNodes": -261,
"jsHeapTotalBytes": 17207296,
"scriptDurationMs": 104.661,
"eventListeners": -123,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "minimap-idle",
"durationMs": 2020.799000000011,
"styleRecalcs": 8,
"styleRecalcDurationMs": 7.896,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 563.9970000000001,
"heapDeltaBytes": 2075416,
"heapUsedBytes": 70037104,
"domNodes": -262,
"jsHeapTotalBytes": 10592256,
"scriptDurationMs": 96.82499999999999,
"eventListeners": -127,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 588.6480000000347,
"styleRecalcs": 48,
"styleRecalcDurationMs": 12.184999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 375.319,
"heapDeltaBytes": 6881052,
"heapUsedBytes": 50894240,
"domNodes": 21,
"jsHeapTotalBytes": 12582912,
"scriptDurationMs": 124.637,
"eventListeners": 8,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000273
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 563.5540000000105,
"styleRecalcs": 47,
"styleRecalcDurationMs": 11.988,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 376.843,
"heapDeltaBytes": 8239548,
"heapUsedBytes": 58627836,
"domNodes": 19,
"jsHeapTotalBytes": 14155776,
"scriptDurationMs": 127.863,
"eventListeners": 8,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.663333333333338,
"p95FrameDurationMs": 16.700000000000273
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 520.2900000000454,
"styleRecalcs": 46,
"styleRecalcDurationMs": 10.386000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 346.90600000000006,
"heapDeltaBytes": 7236136,
"heapUsedBytes": 51262064,
"domNodes": 18,
"jsHeapTotalBytes": 13369344,
"scriptDurationMs": 114.83099999999999,
"eventListeners": 8,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-idle",
"durationMs": 1990.9420000000182,
"styleRecalcs": 8,
"styleRecalcDurationMs": 7.2970000000000015,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 363.75600000000003,
"heapDeltaBytes": 20105200,
"heapUsedBytes": 64380256,
"domNodes": 16,
"jsHeapTotalBytes": 23068672,
"scriptDurationMs": 17.903999999999996,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-idle",
"durationMs": 1992.5790000000347,
"styleRecalcs": 10,
"styleRecalcDurationMs": 9.484000000000002,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 393.99300000000005,
"heapDeltaBytes": 20889284,
"heapUsedBytes": 64998828,
"domNodes": 20,
"jsHeapTotalBytes": 22806528,
"scriptDurationMs": 24.605999999999995,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-idle",
"durationMs": 1989.7220000000289,
"styleRecalcs": 10,
"styleRecalcDurationMs": 9.32,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 407.309,
"heapDeltaBytes": 19953908,
"heapUsedBytes": 64020672,
"domNodes": 20,
"jsHeapTotalBytes": 22806528,
"scriptDurationMs": 23.317999999999998,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1972.3189999999988,
"styleRecalcs": 84,
"styleRecalcDurationMs": 52.989,
"layouts": 16,
"layoutDurationMs": 5.347,
"taskDurationMs": 1026.7279999999998,
"heapDeltaBytes": 11869888,
"heapUsedBytes": 56341064,
"domNodes": 72,
"jsHeapTotalBytes": 22806528,
"scriptDurationMs": 103.65299999999999,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1991.8810000000349,
"styleRecalcs": 84,
"styleRecalcDurationMs": 46.538999999999994,
"layouts": 16,
"layoutDurationMs": 4.757000000000001,
"taskDurationMs": 941.278,
"heapDeltaBytes": 12315724,
"heapUsedBytes": 56240480,
"domNodes": 72,
"jsHeapTotalBytes": 22806528,
"scriptDurationMs": 103.16,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1975.274000000013,
"styleRecalcs": 84,
"styleRecalcDurationMs": 49.666000000000004,
"layouts": 16,
"layoutDurationMs": 4.869,
"taskDurationMs": 961.8610000000001,
"heapDeltaBytes": 12309248,
"heapUsedBytes": 56604012,
"domNodes": 73,
"jsHeapTotalBytes": 23855104,
"scriptDurationMs": 102.21,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "viewport-pan-sweep",
"durationMs": 8138.081999999997,
"styleRecalcs": 250,
"styleRecalcDurationMs": 56.603,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 4008.1629999999996,
"heapDeltaBytes": -1128652,
"heapUsedBytes": 57874520,
"domNodes": -76,
"jsHeapTotalBytes": 21098496,
"scriptDurationMs": 1305.8129999999999,
"eventListeners": -68,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "viewport-pan-sweep",
"durationMs": 8129.129999999976,
"styleRecalcs": 249,
"styleRecalcDurationMs": 53.19,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 3891.354,
"heapDeltaBytes": 16829572,
"heapUsedBytes": 69975856,
"domNodes": -262,
"jsHeapTotalBytes": 20295680,
"scriptDurationMs": 1271.4669999999999,
"eventListeners": -111,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "viewport-pan-sweep",
"durationMs": 8188.031000000024,
"styleRecalcs": 251,
"styleRecalcDurationMs": 54.337999999999994,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 3838.8100000000004,
"heapDeltaBytes": 22724168,
"heapUsedBytes": 75306144,
"domNodes": -259,
"jsHeapTotalBytes": 20033536,
"scriptDurationMs": 1272.316,
"eventListeners": -111,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.669999999999952,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "vue-large-graph-idle",
"durationMs": 10788.685999999983,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 10775.095000000001,
"heapDeltaBytes": -50304928,
"heapUsedBytes": 166710780,
"domNodes": -9850,
"jsHeapTotalBytes": 21557248,
"scriptDurationMs": 590.8910000000001,
"eventListeners": -23962,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.220000000000073,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "vue-large-graph-idle",
"durationMs": 11069.491000000027,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 11020.983,
"heapDeltaBytes": -41324768,
"heapUsedBytes": 156120852,
"domNodes": -9850,
"jsHeapTotalBytes": -28774400,
"scriptDurationMs": 639.161,
"eventListeners": -23965,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.219999999999953,
"p95FrameDurationMs": 16.80000000000291
},
{
"name": "vue-large-graph-idle",
"durationMs": 10914.434000000028,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 10900.104000000001,
"heapDeltaBytes": -64070080,
"heapUsedBytes": 160555368,
"domNodes": -9852,
"jsHeapTotalBytes": 15917056,
"scriptDurationMs": 575.4830000000001,
"eventListeners": -23965,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.219999999999953,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "vue-large-graph-pan",
"durationMs": 13064.82500000004,
"styleRecalcs": 67,
"styleRecalcDurationMs": 18.137999999999987,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 13044.618999999997,
"heapDeltaBytes": -63368692,
"heapUsedBytes": 162550984,
"domNodes": -9850,
"jsHeapTotalBytes": -12259328,
"scriptDurationMs": 892.328,
"eventListeners": -23960,
"totalBlockingTimeMs": 60,
"frameDurationMs": 17.223333333333358,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "vue-large-graph-pan",
"durationMs": 13103.02999999999,
"styleRecalcs": 65,
"styleRecalcDurationMs": 18.154000000000003,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 13056.247999999998,
"heapDeltaBytes": -51616988,
"heapUsedBytes": 174358324,
"domNodes": -9850,
"jsHeapTotalBytes": -25452544,
"scriptDurationMs": 892.0920000000001,
"eventListeners": -23957,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.776666666666642,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "vue-large-graph-pan",
"durationMs": 12890.629999999986,
"styleRecalcs": 66,
"styleRecalcDurationMs": 18.04,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 12862.792999999998,
"heapDeltaBytes": -51065328,
"heapUsedBytes": 161885800,
"domNodes": -9850,
"jsHeapTotalBytes": -12521472,
"scriptDurationMs": 868.139,
"eventListeners": -23959,
"totalBlockingTimeMs": 55,
"frameDurationMs": 17.77333333333336,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "workflow-execution",
"durationMs": 138.00699999995913,
"styleRecalcs": 11,
"styleRecalcDurationMs": 19.349999999999998,
"layouts": 5,
"layoutDurationMs": 1.6700000000000002,
"taskDurationMs": 110.47200000000001,
"heapDeltaBytes": 3457116,
"heapUsedBytes": 55014268,
"domNodes": 149,
"jsHeapTotalBytes": 262144,
"scriptDurationMs": 19.766,
"eventListeners": 37,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.663333333333338,
"p95FrameDurationMs": 16.700000000000273
},
{
"name": "workflow-execution",
"durationMs": 462.87900000004356,
"styleRecalcs": 16,
"styleRecalcDurationMs": 22.538000000000004,
"layouts": 5,
"layoutDurationMs": 1.451,
"taskDurationMs": 121.04099999999998,
"heapDeltaBytes": 4960856,
"heapUsedBytes": 49979680,
"domNodes": 154,
"jsHeapTotalBytes": 0,
"scriptDurationMs": 23.534000000000002,
"eventListeners": 71,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000273
},
{
"name": "workflow-execution",
"durationMs": 462.89600000000064,
"styleRecalcs": 14,
"styleRecalcDurationMs": 24.495,
"layouts": 5,
"layoutDurationMs": 1.5729999999999997,
"taskDurationMs": 126.384,
"heapDeltaBytes": 4965988,
"heapUsedBytes": 50278148,
"domNodes": 152,
"jsHeapTotalBytes": 262144,
"scriptDurationMs": 25.984,
"eventListeners": 71,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000273
}
]
} |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
src/platform/workspace/stores/useWorkspaceAuth.test.ts (1)
734-742: AddEXPIRES_ATassertions in the new persistence/race checksThese updated tests validate
CURRENT_WORKSPACEandTOKEN, but notEXPIRES_AT. Since stale commits also write expiry, asserting it here would close the regression surface for stale overwrite/preserve behavior.Suggested assertion additions
expect(sessionStorage.getItem(WORKSPACE_STORAGE_KEYS.TOKEN)).toBe( 'workspace-token-abc' ) + expect( + sessionStorage.getItem(WORKSPACE_STORAGE_KEYS.EXPIRES_AT) + ).toBeTruthy()expect(sessionStorage.getItem(WORKSPACE_STORAGE_KEYS.TOKEN)).toBe( 'new-workspace-token' ) + expect( + sessionStorage.getItem(WORKSPACE_STORAGE_KEYS.EXPIRES_AT) + ).toBeTruthy()As per coding guidelines "Write tests for all changes, especially bug fixes to catch future regressions."
Also applies to: 823-828, 841-846
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/platform/workspace/stores/useWorkspaceAuth.test.ts` around lines 734 - 742, Add assertions that the expiry value is persisted and preserved: after the existing checks on currentWorkspace and workspaceToken, assert that sessionStorage.getItem(WORKSPACE_STORAGE_KEYS.EXPIRES_AT) equals the expected expiry value used in the test setup (e.g. the mock expiry value or mockWorkspaceWithRole.expiresAt string), and if the test stores expiry as JSON ensure you compare the same serialized form; apply the same EXPIRES_AT assertion in the other two assertion blocks referenced (the similar checks around the other transient/persistence scenarios).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/platform/workspace/stores/workspaceAuthStore.ts`:
- Around line 348-354: When the transient-failure branch (isTransientError &&
hasValidWorkspaceToken()) decides to preserve the existing token, do not return
while the last thrown WorkspaceAuthError remains set and no future refresh is
scheduled; instead clear the stored error state (reset the local/instance
"error" / "workspaceAuthError" variable or call the store's clearError helper)
and enqueue a future refresh attempt by invoking the store's refresh scheduling
helper (e.g., scheduleRefresh, scheduleWorkspaceRefresh, or re-arm the existing
timer to call switchWorkspace again after a backoff). This preserves the valid
token, removes the lingering error, and ensures a proactive retry without
changing the branch's intent.
---
Nitpick comments:
In `@src/platform/workspace/stores/useWorkspaceAuth.test.ts`:
- Around line 734-742: Add assertions that the expiry value is persisted and
preserved: after the existing checks on currentWorkspace and workspaceToken,
assert that sessionStorage.getItem(WORKSPACE_STORAGE_KEYS.EXPIRES_AT) equals the
expected expiry value used in the test setup (e.g. the mock expiry value or
mockWorkspaceWithRole.expiresAt string), and if the test stores expiry as JSON
ensure you compare the same serialized form; apply the same EXPIRES_AT assertion
in the other two assertion blocks referenced (the similar checks around the
other transient/persistence scenarios).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 2092171e-46dd-4ce0-9280-881cffb78c34
📒 Files selected for processing (2)
src/platform/workspace/stores/useWorkspaceAuth.test.tssrc/platform/workspace/stores/workspaceAuthStore.ts
Codecov Report❌ Patch coverage is
@@ Coverage Diff @@
## main #11726 +/- ##
===========================================
- Coverage 69.55% 51.55% -18.01%
===========================================
Files 1485 1376 -109
Lines 83671 70377 -13294
Branches 23029 19567 -3462
===========================================
- Hits 58201 36280 -21921
- Misses 24525 33498 +8973
+ Partials 945 599 -346
Flags with carried forward coverage won't be shown. Click here to find out more.
... and 1003 files with indirect coverage changes 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c3bcc28fbc
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (capturedRequestId !== refreshRequestId) { | ||
| console.warn( | ||
| 'Aborting stale workspace switch: workspace context changed before commit' | ||
| ) | ||
| return |
There was a problem hiding this comment.
Don't discard old-workspace refresh when new switch fails
This guard drops any in-flight refresh response as soon as refreshRequestId changes, but refreshRequestId is incremented before a new switchWorkspace request is known to succeed. If refreshToken() for workspace A is in flight, then switchWorkspace('B') fails (e.g. 403), workspace A remains active but the successful refresh response for A is discarded here; refreshToken() then returns without committing a new token/schedule, so A can continue with an aging token until expiration. The stale check should also account for whether currentWorkspace actually changed away from the refreshed workspace before aborting.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
src/platform/workspace/stores/useWorkspaceAuth.test.ts (1)
825-833: Strengthen stale-race regression by assertingEXPIRES_ATis not clobberedThe test currently protects
CURRENT_WORKSPACEandTOKEN, but notEXPIRES_AT. Using distinct expiries for “new” vs “stale” responses would catch expiry-key overwrites too.✅ Suggested test tightening
- mockFetch.mockResolvedValueOnce({ + const newExpiry = new Date(Date.now() + 7200 * 1000).toISOString() + mockFetch.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ ...mockTokenResponse, token: 'new-workspace-token', + expires_at: newExpiry, workspace: newWorkspace }) }) @@ expect(sessionStorage.getItem(WORKSPACE_STORAGE_KEYS.TOKEN)).toBe( 'new-workspace-token' ) + const expectedNewExpiryMs = new Date(newExpiry).getTime().toString() + expect(sessionStorage.getItem(WORKSPACE_STORAGE_KEYS.EXPIRES_AT)).toBe( + expectedNewExpiryMs + ) @@ expect(sessionStorage.getItem(WORKSPACE_STORAGE_KEYS.TOKEN)).toBe( 'new-workspace-token' ) + expect(sessionStorage.getItem(WORKSPACE_STORAGE_KEYS.EXPIRES_AT)).toBe( + expectedNewExpiryMs + )As per coding guidelines, "Write tests for all changes, especially bug fixes to catch future regressions".
Also applies to: 857-862
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/platform/workspace/stores/useWorkspaceAuth.test.ts` around lines 825 - 833, The test currently only protects CURRENT_WORKSPACE and TOKEN but not EXPIRES_AT; update the mocked responses (the mockTokenResponse used in mockFetch.mockResolvedValueOnce and the subsequent mock) to include distinct expiry values (e.g., expiresAt: 'stale-ts' vs 'new-ts' or numeric EXPIRES_AT) for the "stale" and "new" responses and then add assertions that the stored EXPIRES_AT value (the key your code writes, e.g., EXPIRES_AT) equals the expected new expiry after the race-resolve path and was not clobbered by the stale response; modify both places the mock is set (the block using mockTokenResponse at the shown diff and the later similar mock at the other occurrence) and add an expect(...) asserting EXPIRES_AT remains the new value.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/platform/workspace/stores/workspaceAuthStore.ts`:
- Around line 202-203: The loading state can be cleared by stale overlapping
calls to switchWorkspace; modify switchWorkspace to generate a unique request
id/token at start (set isLoading.value = true; error.value = null), store it on
the store (e.g., currentSwitchId), and capture it in the async call; only clear
isLoading.value and update error.value when the captured id matches the
store.currentSwitchId. Apply the same request-id check to the success and error
branches where isLoading is set to false (the code around switchWorkspace and
the lines that currently set isLoading.value = false / error.value = ...).
---
Nitpick comments:
In `@src/platform/workspace/stores/useWorkspaceAuth.test.ts`:
- Around line 825-833: The test currently only protects CURRENT_WORKSPACE and
TOKEN but not EXPIRES_AT; update the mocked responses (the mockTokenResponse
used in mockFetch.mockResolvedValueOnce and the subsequent mock) to include
distinct expiry values (e.g., expiresAt: 'stale-ts' vs 'new-ts' or numeric
EXPIRES_AT) for the "stale" and "new" responses and then add assertions that the
stored EXPIRES_AT value (the key your code writes, e.g., EXPIRES_AT) equals the
expected new expiry after the race-resolve path and was not clobbered by the
stale response; modify both places the mock is set (the block using
mockTokenResponse at the shown diff and the later similar mock at the other
occurrence) and add an expect(...) asserting EXPIRES_AT remains the new value.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: ed5c4973-a24b-48e6-b915-1eb4a76db3c3
📒 Files selected for processing (2)
src/platform/workspace/stores/useWorkspaceAuth.test.tssrc/platform/workspace/stores/workspaceAuthStore.ts
| isLoading.value = true | ||
| error.value = null |
There was a problem hiding this comment.
isLoading can be cleared by a stale request while a newer switch is still pending
Line 306 always sets isLoading to false, so with overlapping switchWorkspace calls, an older stale request can hide loading state for the active request.
💡 Proposed fix (track in-flight switches)
// Timer state
let refreshTimerId: ReturnType<typeof setTimeout> | null = null
+ let inFlightSwitchCount = 0
@@
- isLoading.value = true
+ inFlightSwitchCount += 1
+ isLoading.value = true
error.value = null
@@
} finally {
- isLoading.value = false
+ inFlightSwitchCount = Math.max(0, inFlightSwitchCount - 1)
+ isLoading.value = inFlightSwitchCount > 0
}
}Also applies to: 305-307
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/platform/workspace/stores/workspaceAuthStore.ts` around lines 202 - 203,
The loading state can be cleared by stale overlapping calls to switchWorkspace;
modify switchWorkspace to generate a unique request id/token at start (set
isLoading.value = true; error.value = null), store it on the store (e.g.,
currentSwitchId), and capture it in the async call; only clear isLoading.value
and update error.value when the captured id matches the store.currentSwitchId.
Apply the same request-id check to the success and error branches where
isLoading is set to false (the code around switchWorkspace and the lines that
currently set isLoading.value = false / error.value = ...).
| } | ||
|
|
||
| const timeUntilExpiry = workspaceTokenExpiresAt.value - Date.now() | ||
| if (timeUntilExpiry <= 0) { |
There was a problem hiding this comment.
issue: when timeUntilExpiry is already <= 0 here, the function returns silently. The token is past expiry but currentWorkspace and workspaceToken remain non-null with no scheduled refresh, so isAuthenticated stays true while the backend will reject the token.
Reachable after 4 fetch attempts plus 1+2+4s backoff (~7s) on a slow network when the token had little headroom. Could clearWorkspaceContext() be called here instead of returning silently? The preserve-token contract only holds while the token is still valid.
| () => { | ||
| void refreshToken() | ||
| }, | ||
| Math.min(delayMs, timeUntilExpiry) |
There was a problem hiding this comment.
suggestion (non-blocking): scheduleTokenRefresh refreshes at expiresAt - TOKEN_REFRESH_BUFFER_MS, but this retry path schedules at up to expiresAt itself. A retry firing within the buffer window has near-zero margin. Would using Math.min(delayMs, timeUntilExpiry - TOKEN_REFRESH_BUFFER_MS) keep the two scheduling policies aligned?
| if (capturedRequestId === refreshRequestId) { | ||
| if (isTransientError && hasValidWorkspaceToken()) { | ||
| error.value = null | ||
| scheduleTokenRefreshRetry(baseDelayMs * Math.pow(2, maxRetries)) |
There was a problem hiding this comment.
suggestion (non-blocking): the preserve branch reschedules another retry on every failure, with no max-attempts ceiling across scheduled retries and no jitter. Against a backend returning 500 for the full token lifetime this can produce many hundreds of fetches and warn entries before the token finally expires. Would tracking consecutiveScheduledRetries and giving up after N (e.g. 3-5) consecutive failures be reasonable?
| // State | ||
| const currentWorkspace = shallowRef<WorkspaceWithRole | null>(null) | ||
| const workspaceToken = ref<string | null>(null) | ||
| const workspaceTokenExpiresAt = ref<number | null>(null) |
There was a problem hiding this comment.
nitpick (non-blocking): workspaceTokenExpiresAt is not exported and not read by any computed/watch (only imperatively from scheduleTokenRefreshRetry and hasValidWorkspaceToken). The sibling internal-control variables refreshRequestId and refreshTimerId are plain let. Would let workspaceTokenExpiresAt: number | null = null be more consistent and avoid the .value noise?
| // Only clear context if this refresh is still for the current workspace | ||
| if (capturedRequestId === refreshRequestId) { | ||
| if (isTransientError && hasValidWorkspaceToken()) { | ||
| error.value = null |
There was a problem hiding this comment.
suggestion (non-blocking): error.value is never written by the transient retry path inside the loop, so this null-out has nothing to clear. If the intent is to clear a leftover error from a prior failure, would clearing it at the top of refreshToken() be cleaner?
| } catch (err) { | ||
| if (capturedRequestId !== refreshRequestId) { | ||
| console.warn( | ||
| 'Aborting stale workspace switch: workspace context changed before error commit' |
There was a problem hiding this comment.
nitpick (non-blocking): would including err in this warn payload help diagnostics? The underlying failure may indicate a real backend problem worth logging, e.g. console.warn('Aborting stale workspace switch: workspace context changed before error commit', err).
| // (e.g. compare captured requestId or expected workspaceId before | ||
| // assigning state). Removing `.fails` once fixed will catch regressions. | ||
| it.fails('the new workspace wins when the stale refresh resolves last', async () => { | ||
| it('the new workspace wins when the stale refresh resolves last', async () => { |
There was a problem hiding this comment.
suggestion (non-blocking): the new catch-block ABA guard (workspaceAuthStore.ts:296-301) is not exercised by tests. This race test resolves the stale fetch with ok: true, which only covers the success-path guard. Would adding a test where the hung refresh fetch rejects or returns 500 after a new switchWorkspace has committed help lock in this branch?
| Promise.resolve({ ...mockTokenResponse, token: 'retry-token' }) | ||
| }) | ||
|
|
||
| await vi.advanceTimersByTimeAsync(7999) |
There was a problem hiding this comment.
nitpick (non-blocking): the 7999 / 1 split asserts the scheduled retry fires at exactly 8000ms (= baseDelayMs * 2^maxRetries). Would a one-line comment naming the formula make a future failure here self-explanatory?
| ), | ||
| clear: originalSessionStorage.clear.bind(originalSessionStorage) | ||
| } satisfies Storage | ||
| vi.stubGlobal('sessionStorage', throwingSessionStorage) |
There was a problem hiding this comment.
nitpick (non-blocking): this stubGlobal swap-out is heavier than the original vi.spyOn(sessionStorage, 'setItem'). If happy-dom Storage-prototype spy flakiness is the motivation, would a one-line comment help future readers? Otherwise reverting to spyOn would keep the test smaller.
Summary
Fixes FE-485.
This updates workspace auth refresh handling so stale in-flight refresh responses cannot overwrite a newer workspace context, and exhausted transient token exchange failures preserve the existing workspace context while its token is still valid.
Changes
switchWorkspacewrites workspace state, workspace token,error, orsessionStorage.sessionStoragepreservation.Browser / E2E coverage
No Playwright test was added because this bug is in the Pinia store race between mocked token-exchange promises, request IDs, token expiry, and
sessionStoragecommits. The deterministic unit spec directly controls the ordering that is not practical to force through the browser without real auth/session infrastructure and artificial network timing hooks.Validation
pnpm format -- src/platform/workspace/stores/workspaceAuthStore.ts src/platform/workspace/stores/useWorkspaceAuth.test.tspnpm exec vitest run src/platform/workspace/stores/useWorkspaceAuth.test.tspnpm exec eslint src/platform/workspace/stores/workspaceAuthStore.ts src/platform/workspace/stores/useWorkspaceAuth.test.tspnpm exec oxlint src/platform/workspace/stores/workspaceAuthStore.ts src/platform/workspace/stores/useWorkspaceAuth.test.ts --type-awarepnpm exec vue-tsc --noEmit --pretty false┆Issue is synchronized with this Notion page by Unito